﻿@page "/settings/security/change-password"
@using AliasVault.Client.Utilities
@using AliasVault.Client.Main.Models.Validation
@using AliasVault.Shared.Models.WebApi.PasswordChange
@using AliasVault.Shared.Models.WebApi.Vault;
@using AliasVault.Cryptography.Client
@using SecureRemotePassword
@using Microsoft.Extensions.Localization
@inherits MainBase
@inject HttpClient Http

<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>

<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
    <div class="mb-4 col-span-full xl:mb-2">
        <Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
        <H1>@Localizer["PageTitle"]</H1>
        <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["PageDescription"]</p>
    </div>
</div>

@if (IsLoading)
{
    <LoadingIndicator />
}
else
{
    <div class="p-4 mb-4  mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
        <EditForm Model="@PasswordChangeFormModel" OnValidSubmit="@InitiatePasswordChange" class="space-y-4">
            <DataAnnotationsValidator />
            <ValidationSummary />

            <div>
                <label for="currentPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["CurrentPasswordLabel"]</label>
                <InputText type="password" id="currentPassword" @bind-Value="PasswordChangeFormModel.CurrentPassword"
                           class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
                           required />
            </div>

            <div>
                <label for="newPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["NewPasswordLabel"]</label>
                <InputText type="password" id="newPassword" @bind-Value="PasswordChangeFormModel.NewPassword"
                           class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
                           required />
            </div>

            <div>
                <label for="newPasswordConfirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmNewPasswordLabel"]</label>
                <InputText type="password" id="newPasswordConfirm" @bind-Value="PasswordChangeFormModel.NewPasswordConfirm"
                           class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
                           required />
            </div>

            <button type="submit"
                    class="w-full bg-primary-500 text-white py-2 px-4 rounded-md hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition duration-150 ease-in-out">
                @Localizer["ChangePasswordButton"]
            </button>
        </EditForm>
    </div>
}

@code {
    /// <summary>
    /// Gets or sets a value indicating whether the component is loading.
    /// </summary>
    private bool IsLoading { get; set; } = true;

    /// <summary>
    /// Gets or sets the password change form model.
    /// </summary>
    private PasswordChangeFormModel PasswordChangeFormModel { get; set; } = new();

    private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.ChangePassword", "AliasVault.Client");
    private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");

    /// <summary>
    /// Gets or sets the current user's password salt.
    /// </summary>
    private string CurrentSalt { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the current user's password server ephemeral.
    /// </summary>
    private string CurrentServerEphemeral { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the current user's password encryption type.
    /// </summary>
    private string CurrentEncryptionType { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the current user's password encryption settings.
    /// </summary>
    private string CurrentEncryptionSettings { get; set; } = string.Empty;

    private SrpEphemeral ClientEphemeral = new();
    private SrpSession ClientSession = new();

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" });
        BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbChangePassword"] });
    }

    /// <inheritdoc />
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);

        // Get the current password ephemeral and salt from the server
        // which is required to confirm the current password.
        if (firstRender)
        {
            await GetCurrentPasswordEphemeralAndSalt();

            IsLoading = false;
            StateHasChanged();
        }
    }

    /// <summary>
    /// Gets the current password ephemeral and salt from the server which
    /// is required to confirm the current password.
    /// </summary>
    private async Task GetCurrentPasswordEphemeralAndSalt()
    {
        var response = await Http.GetFromJsonAsync<PasswordChangeInitiateResponse>("v1/Auth/change-password/initiate");

        if (response == null)
        {
            GlobalNotificationService.AddErrorMessage(Localizer["FailedToInitiatePasswordChange"], true);
            IsLoading = false;
            StateHasChanged();
            return;
        }

        CurrentServerEphemeral = response.ServerEphemeral;
        CurrentSalt = response.Salt;
        CurrentEncryptionType = response.EncryptionType;
        CurrentEncryptionSettings = response.EncryptionSettings;
    }

    /// <summary>
    /// Initiates the password change process.
    /// </summary>
    private async Task InitiatePasswordChange()
    {
        GlobalLoadingSpinner.Show(Localizer["ChangingPasswordMessage"]);
        GlobalNotificationService.ClearMessages();
        StateHasChanged();

        // Generate ephemeral for current password to verify it.
        var currentPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.CurrentPassword, CurrentSalt, CurrentEncryptionType, CurrentEncryptionSettings);
        var currentPasswordHashString = BitConverter.ToString(currentPasswordHash).Replace("-", string.Empty);

        ClientEphemeral = Srp.GenerateEphemeralClient();
        var username = await GetUsernameAsync();
        var privateKey = Srp.DerivePrivateKey(CurrentSalt, username, currentPasswordHashString);
        ClientSession = Srp.DeriveSessionClient(
            privateKey,
            ClientEphemeral.Secret,
            CurrentServerEphemeral,
            CurrentSalt,
            username);

        // Generate salt and verifier for new password.
        var client = new SrpClient();
        var newSalt = client.GenerateSalt();

        byte[] newPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.NewPassword, newSalt);
        var newPasswordHashString = BitConverter.ToString(newPasswordHash).Replace("-", string.Empty);

        // Backup current password hash in case of failure.
        var backupPasswordHash = AuthService.GetEncryptionKey();

        // Set new currentPasswordHash locally as it is required for the new database encryption call below so
        // it is encrypted with the new password hash.
        await AuthService.StoreEncryptionKeyAsync(newPasswordHash);

        var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, username, newPasswordHashString);

        // Prepare new vault model to update to.
        var encryptedBase64String = await DbService.GetEncryptedDatabaseBase64String();
        var vault = await DbService.PrepareVaultForUploadAsync(encryptedBase64String);

        var vaultPasswordChangeObject = new VaultPasswordChangeRequest
        {
            Username = username,
            Blob = vault.Blob,
            Version = vault.Version,
            CurrentRevisionNumber = vault.CurrentRevisionNumber,
            EncryptionPublicKey = vault.EncryptionPublicKey,
            CredentialsCount = vault.CredentialsCount,
            EmailAddressList = vault.EmailAddressList,
            PrivateEmailDomainList = [],
            HiddenPrivateEmailDomainList = [],
            PublicEmailDomainList = [],
            CreatedAt = vault.CreatedAt,
            UpdatedAt = vault.UpdatedAt,
            CurrentClientPublicEphemeral = ClientEphemeral.Public,
            CurrentClientSessionProof = ClientSession.Proof,
            NewPasswordSalt = srpPasswordChange.Salt,
            NewPasswordVerifier = srpPasswordChange.Verifier
        };

        // Clear form.
        PasswordChangeFormModel = new PasswordChangeFormModel();

        // 4. Client sends proof of session key to server.
        try {
            var response = await Http.PostAsJsonAsync("v1/Vault/change-password", vaultPasswordChangeObject);
            var responseContent = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer))
                {
                    GlobalNotificationService.AddErrorMessage(error, true);
                }

                // Set currentPasswordHash back to original, so we're back to the original state.
                await AuthService.StoreEncryptionKeyAsync(backupPasswordHash);

                GlobalLoadingSpinner.Hide();
                StateHasChanged();
                return;
            }

            // Deserialize the response content as a VaultUpdateResponse in case of success.
            var vaultUpdateResponse = await response.Content.ReadFromJsonAsync<VaultUpdateResponse>();

            if (vaultUpdateResponse != null)
            {
                DbService.StoreVaultRevisionNumber(vaultUpdateResponse.NewRevisionNumber);
            }
        }
        catch
        {
            GlobalNotificationService.AddErrorMessage(Localizer["FailedToChangePassword"], true);

            // Set currentPasswordHash back to original, so we're back to the original state.
            await AuthService.StoreEncryptionKeyAsync(backupPasswordHash);

            GlobalLoadingSpinner.Hide();
            StateHasChanged();
            return;
        }

        // Set success message.
        GlobalNotificationService.AddSuccessMessage(Localizer["PasswordChangedSuccessfully"], true);

        // Get the new password ephemeral and salt from the server, which is required if the usre
        // wants to change the password again.
        await GetCurrentPasswordEphemeralAndSalt();

        GlobalLoadingSpinner.Hide();
        StateHasChanged();
    }
}
